package org.mtmi.tuio;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.eclipse.swt.widgets.Display;
import org.mtmi.gestures.GestureFactory;
import org.mtmi.gestures.IGesture;
import org.mtmi.ui.controls.ITouchableControl;
import org.mtmi.ui.scenes.SceneManager;

import TUIO.TuioClient;
import TUIO.TuioCursor;
import TUIO.TuioListener;
import TUIO.TuioObject;
import TUIO.TuioTime;

public class TUIOManager implements TuioListener {

	private static TUIOManager instance;
	private TuioClient tuioClient;

	private boolean realTuioMessage = false;

	private final int TRACE_TIME_GAP = 200;
	private final float TRACE_SPACE_GAP = 0.02f;

	// Accumulators for adding, updating and removing cursors.
	// These lists will be cleared at the end of every TuioListener.refresh()
	// call,
	// thus their purpose is merely private to this class
	private List<CursorPoint> addedCursorslist, updatedCursorsList,
			removedCursorsList;

	// List of recently died traces. A trace dies when a REMOVED cursor is added
	// to their path,
	// (that is when the user lifts the finger from the table). Within a time
	// specified by
	// Settings.TRACE_TIME_GAP and a space specified by Settings.TRACE_SPACE_GAP
	// the trace can resurrect,
	// that is a new cursor can be added to the path of the trace so that this
	// reborns.
	private List<Trace> resurrectableTraces;

	// A hashmap to associate tuio cursors' ids with the relative trace
	private HashMap<Integer, Trace> cursorsTracesTable;
	private SceneManager sceneManager;

	private TUIOManager() {
		super();

		addedCursorslist = new ArrayList<CursorPoint>();
		updatedCursorsList = new ArrayList<CursorPoint>();
		removedCursorsList = new ArrayList<CursorPoint>();
		resurrectableTraces = new ArrayList<Trace>();

		cursorsTracesTable = new HashMap<Integer, Trace>();

		tuioClient = new TuioClient();
	}

	public static TUIOManager getInstance() {
		if (instance == null) {
			instance = new TUIOManager();
		}

		return instance;
	}

	public void init(SceneManager sm) {
		sceneManager = sm;
		tuioClient.addTuioListener(this);
		tuioClient.connect();
	}

	public void dispose() {
		tuioClient.removeTuioListener(this);
		tuioClient.disconnect();
	}

	@Override
	public void addTuioCursor(TuioCursor cursor) {
		realTuioMessage = true;
		addedCursorslist.add(new CursorPoint((int) cursor.getSessionID(),
				cursor.getX(), cursor.getY(), cursor.getXSpeed(), cursor
						.getYSpeed(), cursor.getMotionSpeed(), cursor
						.getMotionAccel(), (int) cursor.getTuioTime()
						.getTotalMilliseconds(), CursorPoint.States.ADDED));

	}

	@Override
	public void updateTuioCursor(TuioCursor cursor) {
		realTuioMessage = true;
		updatedCursorsList.add(new CursorPoint((int) cursor.getSessionID(),
				cursor.getX(), cursor.getY(), cursor.getXSpeed(), cursor
						.getYSpeed(), cursor.getMotionSpeed(), cursor
						.getMotionAccel(), (int) cursor.getTuioTime()
						.getTotalMilliseconds(), CursorPoint.States.UPDATED));
	}

	@Override
	public void removeTuioCursor(TuioCursor cursor) {
		realTuioMessage = true;
		removedCursorsList.add(new CursorPoint((int) cursor.getSessionID(),
				cursor.getX(), cursor.getY(), cursor.getXSpeed(), cursor
						.getYSpeed(), cursor.getMotionSpeed(), cursor
						.getMotionAccel(), (int) cursor.getTuioTime()
						.getTotalMilliseconds(), CursorPoint.States.REMOVED));

	}

	@Override
	public void refresh(TuioTime time) {
		int timeStamp = (int) time.getTotalMilliseconds();

		// If a trace is non resurrectable (if the TRACE_TIME_GAP is greater
		// than the time between a removed cursor and added one).
		RemoveNonResurrectableTraces(timeStamp);

		ProcessCurrentRemovingCursors(timeStamp);
		ProcessCurrentUpdatingCursors(timeStamp);
		ProcessCurrentAddingCursors(timeStamp);

		if (realTuioMessage) {

			Iterator<Integer> iterator = cursorsTracesTable.keySet().iterator();
			while (iterator.hasNext()) {
				Trace trace = cursorsTracesTable.get(iterator.next());
				processTrace(trace);

			}
			for (Iterator<Trace> it = resurrectableTraces.iterator(); it
					.hasNext();) {
				Trace t = (Trace) it.next();
				processTrace(t);
			}
		}

		realTuioMessage = false;

		addedCursorslist.clear();
		updatedCursorsList.clear();
		removedCursorsList.clear();

	}

	private void processTrace(Trace trace) {
		IGesture gesture = GestureFactory.getInstance().getGestureFromTrace(trace);
		if (gesture != null) {
			gesture.notifyTarget();
		}
	}

	private void ProcessCurrentRemovingCursors(int timeStamp) {

		for (CursorPoint cursor : removedCursorsList) {

			// set timestamp
			cursor.timestamp = timeStamp;

			// determine trace
			Trace trace = cursorsTracesTable.get(cursor.sessionId);
			trace.addRemovedCursor(cursor);

			// add trace to the resurrectable traces list
			resurrectableTraces.add(0, trace);

			// remove index
			cursorsTracesTable.remove(cursor.sessionId);

		}

	}

	private void ProcessCurrentUpdatingCursors(int timeStamp) {
		for (CursorPoint cursor : updatedCursorsList) {
			// set timestamp
			cursor.timestamp = timeStamp;

			// determine belonging trace
			Trace trace = cursorsTracesTable.get(cursor.sessionId);

			trace.addAddedOrUpdatedCursor(cursor);

		}

	}

	private void ProcessCurrentAddingCursors(int timeStamp) {
		for (CursorPoint cursor : addedCursorslist) {
			// set timestamp
			cursor.timestamp = timeStamp;

			// Targeting
			ITouchableControl target = null;
			target = sceneManager.getScene().getTouchableControl(cursor.cx, cursor.cy);
			
			//- debug: point feedback (can be removed in final version)
			Display.getDefault().asyncExec(new Runnable() {
				@Override
				public void run() {
					sceneManager.getScene().redraw();
				}
			});
			
			// Check if trace is resurrectable
			Trace trace = tryResurrectTrace(cursor);

			if (trace != null) // try finding a resurrecting trace
			{
				// resurrect the trace
				trace.addAddedOrUpdatedCursor(cursor);

			} else {
				// create a new trace
				trace = new Trace(cursor, target);
			}

			// index the cursor
			cursorsTracesTable.put(cursor.sessionId, trace);

		}

	}

	private void RemoveNonResurrectableTraces(int timeStamp) {

		for (Iterator<Trace> it = resurrectableTraces.iterator(); it.hasNext();) {
			Trace t = (Trace) it.next();
			if (timeStamp - t.getLast().timestamp >= TRACE_TIME_GAP) {
				t.terminate();
				it.remove();
			}
		}

	}

	private Trace tryResurrectTrace(CursorPoint cursor) {
		Trace resurrectingTrace = null;
		float minDist = TRACE_SPACE_GAP * TRACE_SPACE_GAP;
		float dist;
		for (Trace trace : resurrectableTraces) {
			dist = trace.getLast().SquareDistance(cursor);
			if (dist < minDist) {
				minDist = dist;
				resurrectingTrace = trace;
			}
		}
		if (resurrectingTrace != null) {
			resurrectableTraces.remove(resurrectingTrace);
		}

		return resurrectingTrace;
	}

	@Override
	public void updateTuioObject(TuioObject arg0) {
	}

	@Override
	public void addTuioObject(TuioObject arg0) {
	}

	@Override
	public void removeTuioObject(TuioObject arg0) {
	}

}
